﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace Babelfish.Javascript
{
    public class JavascriptParser
    {
        public List<JsObject> RootList = new List<JsObject>();
        private string _buffer = string.Empty;
        private List<JsObject> _parseList = new List<JsObject>();
        private char _lastChar = ' ';

        /// <summary>
        /// Parses the specified js string.
        /// </summary>
        /// <param name="jsString">The js string.</param>
        /// <returns></returns>
        public static List<JsObject> Parse(string jsString)
        {
            var parser = new JavascriptParser();

            return parser.ParseString(jsString);
        }

        /// <summary>
        /// Parses the string.
        /// </summary>
        /// <param name="jsString">The js string.</param>
        /// <returns></returns>
        public List<JsObject> ParseString(string jsString)
        {
            var reader = new StringReader(jsString);

            while (true)
            {
                var c = reader.Read();

                if (c == -1)
                    break;

                ParseNextCharacter((char)c);
            }


            return RootList;
        }

        /// <summary>
        /// Gets a value indicating whether [current tag is type comment].
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [current tag is type comment]; otherwise, <c>false</c>.
        /// </value>
        private bool CurrentTagIsTypeComment
        {
            get
            {
                return _parseList.LastOrDefault() != null && _parseList.LastOrDefault().Type == JsObjectType.Comment;
            }
        }


        /// <summary>
        /// Parses the next character.
        /// </summary>
        /// <param name="character">The character.</param>
        private void ParseNextCharacter(char character)
        {

            if (ParseComments(character))
                return;

            if (CurrentTagIsTypeComment)
            {
                _buffer += character;
                _lastChar = character;
                return;
            }

            switch (character)
            {
                case '{':
                    ParseEntryBlock();
                    break;
                case '}':
                    ParseExitBlock();
                    break;
                case ';':
                    ParseTermination();
                    break;
                case ',':
                    ParseComma();
                    break;
                default:
                    _buffer += character;
                    break;
            }

            _lastChar = character;
        }


        private void ParseComma()
        {
            if (_parseList.Count == 0 || _parseList.LastOrDefault().Type != JsObjectType.Object)
            {
                _buffer += ',';
                return;
            }

            var str = _buffer.Trim();

            if (str.IndexOf(':') < 0)
            {
                _buffer += ',';
                return;
            }

            if (str.IndexOf('(') > str.IndexOf(':'))
            {
                _buffer += ',';
                return;
            }

            if(str.ToCharArray().Count(c => c == '[') != str.ToCharArray().Count(c => c == ']'))
            {
                _buffer += ',';
                return;
            }

            var name = str.Substring(0, str.IndexOf(':'));

            if (name.Trim().StartsWith(","))
                name = name.Trim().Substring(1);

            var value = str.Substring(str.IndexOf(':')+1);

            JsObjectType type = JsObjectType.Variable;

            type = value.Trim().StartsWith("{") ? JsObjectType.Object : type;
            type = value.Trim().ToLower().StartsWith("function") ? JsObjectType.Function : type;

            StartTag(type, name);

            _buffer = value;

            EndTag();
        }

        private void ParseExitBlock()
        {
            if (IsBufferString())
            {
                _buffer += "}";
                return;
            }



            if (_parseList.LastOrDefault() != null && _parseList.LastOrDefault().Type == JsObjectType.Object)
            {
                if (_buffer.Trim().StartsWith(","))
                    _buffer = _buffer.Trim().Substring(1);

                ParseComma();
                EndTag();
                return;
            }


            EndTag();
            return;
        }

        /// <summary>
        /// Determines whether [is buffer string].
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if [is buffer string]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsBufferString()
        {
            if (_buffer.IndexOf('"') < 0 && _buffer.IndexOf("\"") < 0)
                return false;

            int i = 0;

            if (_buffer.IndexOf('"') < _buffer.IndexOf("'"))
                i = Regex.Replace(_buffer, "[^\"]+", string.Empty).ToCharArray().Length % 2;
            else
                i = Regex.Replace(_buffer, "[^']+", string.Empty).ToCharArray().Length % 2;

            return i != 0;
        }

        /// <summary>
        /// Parses the termination.
        /// </summary>
        private void ParseTermination()
        {
            if(IsBufferString())
            {
                _buffer += ";";
                return;
            }

            var str = _buffer.Trim();
            var name = string.Empty;

            if (Regex.Replace(_buffer, "[\\s]+", string.Empty).ToLower().StartsWith("for("))
            {
                _buffer += ";";
                return;
            }

            if (!Regex.IsMatch(_buffer, "(var)?\\s*[\\w]+\\s*=[^=]{1}", RegexOptions.IgnoreCase))
            {
                if (Regex.IsMatch(_buffer.Trim(), "var\\s*[\\w,\\s]+\\s*$", RegexOptions.IgnoreCase))
                {
                    name = Regex.Replace(str, "^var\\s", string.Empty, RegexOptions.IgnoreCase);
                    
                    foreach (var n in name.Split(','))
                    {
                        StartTag(JsObjectType.Variable, n);
                        EndTag();
                    }

                    return;
                }

                _buffer = string.Empty;
                return;
            }

            foreach(var s in  InlineVariableSplit(str))
                ParseVariable(s);
        }

        /// <summary>
        /// Parses the variable.
        /// </summary>
        /// <param name="source">The source.</param>
        private void ParseVariable(string source)
        {
            if (string.IsNullOrEmpty(source))
                return;

            var name = Regex.Replace(source, "^var\\s", string.Empty, RegexOptions.IgnoreCase);
            
            if(name.IndexOf('=') > -1)
                name = name.Substring(0, name.IndexOf('='));
            
            name = Regex.Replace(name, "[^\\w]+", string.Empty, RegexOptions.IgnoreCase);

            var value = Regex.Replace(source, "^var\\s", string.Empty, RegexOptions.IgnoreCase);
            value = Regex.Replace(value, "^.*=", string.Empty, RegexOptions.IgnoreCase);

            StartTag(JsObjectType.Variable, name);
            _buffer = value;
            EndTag();
        }

        private List<string> InlineVariableSplit(string source)
        {
            List<string> list = new List<string>();

            var buffer = string.Empty;

            var tmp = source;

            while (source.Length > 0)
            {
                if (source.IndexOf(',') < 0)
                    break;

                buffer += source.Substring(0, source.IndexOf(',')+1);
                source = source.Remove(0, source.IndexOf(',')+1);

                var c = buffer.IndexOf('"') < buffer.IndexOf("'") ? '"' : '\'';

                var fnutt = buffer.IndexOf("'");
                var dfnutt = buffer.IndexOf('"');

                if (fnutt > -1)
                    if (dfnutt > -1 && dfnutt < fnutt)
                        c = '"';
                    else
                        c = '\'';

                if (dfnutt > -1)
                    if (fnutt > -1 && fnutt < dfnutt)
                        c = '\'';
                    else
                        c = '"';


                if ((buffer.IndexOf('"') < 0 && buffer.IndexOf("'") < 0))
                {
                    list.Add(buffer.Substring(0,buffer.Length-1).Trim());
                    buffer = string.Empty;
                }
                else if (buffer.ToCharArray().Count(ch => ch == c) % 2 == 0)
                {
                    list.Add(buffer.Substring(0, buffer.Length - 1).Trim());
                    buffer = string.Empty;
                }

            }

            if (list.Count == 0)
                list.Add(tmp);


            return list;
        }

        /// <summary>
        /// Parses the entry block.
        /// </summary>
        private void ParseEntryBlock()
        {
            if (IsBufferString())
            {
                _buffer += "{";
                return;
            }

            _buffer = _buffer.Trim();

            var isExpression = false;

            isExpression = Regex.IsMatch(_buffer, "(IF|WHILE|FOR)\\s*\\(.*\\)", RegexOptions.IgnoreCase);

            if (isExpression)
            {
                var name = _buffer.Substring(0, _buffer.IndexOf("(")).Trim();
                StartTag(JsObjectType.Expression, name);
                return;
            }

            var isFunction = false;
            isFunction = _buffer.Trim().ToLower().StartsWith("function") || Regex.IsMatch(_buffer.Trim(),"[\\w]+:function.*", RegexOptions.IgnoreCase);

            if (isFunction)
            {
                var str = Regex.Replace(_buffer.Trim(), "function", string.Empty, RegexOptions.IgnoreCase);
                var name = Regex.Replace(str, "\\([.]*\\)", string.Empty, RegexOptions.IgnoreCase);

                if(name.IndexOf('(') > -1)
                    name = name.Substring(0, name.IndexOf('('));

                StartTag(JsObjectType.Function, name);
                return;
            }

            isFunction = Regex.IsMatch(_buffer, "[\\w]+[\\s]*=[\\s]*function[\\s]*", RegexOptions.IgnoreCase);
            if (isFunction)
            {
                var str = Regex.Replace(_buffer.Trim(), "function", string.Empty, RegexOptions.IgnoreCase);
                var name = Regex.Replace(str, "^var\\s", string.Empty, RegexOptions.IgnoreCase);
                //name = Regex.Replace(name, "[^\\w]+", string.Empty, RegexOptions.IgnoreCase);    
                name = name.Substring(0, name.IndexOf('='));
                name = Regex.Replace(name.Trim(), "\\([.]*\\)", string.Empty, RegexOptions.IgnoreCase);
                StartTag(JsObjectType.Function, name);
                return;
            }

            var isVariable = Regex.IsMatch(_buffer, "[\\w]+[\\s]*=[\\s]*", RegexOptions.IgnoreCase);

            if (isVariable)
            {
                var str = Regex.Replace(_buffer.Trim(), "function", string.Empty, RegexOptions.IgnoreCase);
                var name = Regex.Replace(str, "^var\\s", string.Empty, RegexOptions.IgnoreCase);
                //name = Regex.Replace(name, "[^\\w]+", string.Empty, RegexOptions.IgnoreCase);
                name = name.Substring(0, name.IndexOf('='));
                name = Regex.Replace(name.Trim(), "\\([.]*\\)", string.Empty, RegexOptions.IgnoreCase);
                StartTag(JsObjectType.Object, name);
                return;
            }

            var isObjectNot = Regex.IsMatch(_buffer, "[\\w\\s]+:", RegexOptions.IgnoreCase);

            if (isObjectNot)
            {
                var str = Regex.Replace(_buffer.Trim(), "function", string.Empty, RegexOptions.IgnoreCase);
                var name = str.Substring(0, str.IndexOf(':'));
                StartTag(JsObjectType.Object, name);
                return;
            }

            //end up here? dump it
            _buffer = string.Empty;
        }


        /// <summary>
        /// Parses the comments.
        /// </summary>
        /// <returns></returns>
        private bool ParseComments(char character)
        {

            var tmp = Regex.Replace(_buffer+character, "\\s+", string.Empty);

            //multiline comment START
            if (!CurrentTagIsTypeComment && ((character == '*' && _lastChar == '/') || tmp.StartsWith("/*")))
            {
                _buffer = _buffer.Remove(_buffer.Length - 1);
                StartTag(JsObjectType.Comment, "/*");
                return true;
            }

            //single line comment START
            if (!CurrentTagIsTypeComment && ((character == '/' && _lastChar == '/') || tmp.StartsWith("//")))
            {
                _buffer = _buffer.Remove(_buffer.Length - 1);
                StartTag(JsObjectType.Comment, "//");
                return true;
            }

            //multiline comment STOP
            if (CurrentTagIsTypeComment && ((character == '/' && _lastChar == '*') || tmp.EndsWith("*/")) && _parseList.LastOrDefault().Name == "/*")
            {
                _buffer = _buffer.Remove(_buffer.Length - 1);
                EndTag();
                return true;
            }

            //single line comment STOP
            if (CurrentTagIsTypeComment && character == '\n' && _parseList.LastOrDefault().Name == "//")
            {
                EndTag();
                return true;
            }

            return false;
        }

        /// <summary>
        /// Ends the tag.
        /// </summary>
        private void EndTag()
        {
            //no tag has been started
            if (_parseList.Count == 0)
                return;

            var entity = _parseList[_parseList.Count - 1];

            //if the tag is a property, comment or import we wanna know whats inside
            if (entity.Type != JsObjectType.Function && entity.Type != JsObjectType.Object)
                entity.Value = _buffer;

            if (_parseList.Count == 0)
                return;

            entity = _parseList[_parseList.Count - 1];

            if (entity.Parent == null)
                RootList.Add(entity);

            _parseList.Remove(entity);

            _buffer = string.Empty;
        }

        /// <summary>
        /// Starts the tag.
        /// </summary>
        /// <param name="type">The type.</param>
        private void StartTag(JsObjectType type, string name)
        {
            var parent = _parseList.LastOrDefault();

            var entity = new JsObject { Name = name.Replace(":","").Trim(), Type = type, Children = new List<JsObject>() };

            entity.Name = entity.Name.Trim().StartsWith(",") ? entity.Name.Trim().Substring(1).Trim() : entity.Name;

            if (parent != null)
            {
                entity.Parent = parent;
                _parseList.Add(entity);
                parent.Children.Add(_parseList.LastOrDefault());
            }
            else
            {
                _parseList.Add(entity);
            }

            _buffer = string.Empty;
        }
    }
}
